#include <cmath>

#include <GL/glew.h>
#include <graphics/cgraphicsmanager.h>
#include <game/cgame.h>
#include <utils/cutils.h>
#include <utils/cconfig.h>
#include <physfs/physfs.h>
#include <GL/glfw.h>

std::string CGraphicsManager::m_ErrorString;

/** Default Constructor
 */

CGraphicsManager::CGraphicsManager(): m_FullScreen(true), m_WindowWidth(640), m_WindowHeight(480), m_RedBits(8), m_GreenBits(8), m_BlueBits(8), m_AlphaBits(8), m_DepthBits(0), m_StencilBits(0)
{

}

/** Default Destructor
 */

CGraphicsManager::~CGraphicsManager()
{

}

/** Default Copy Constructor
 */

CGraphicsManager::CGraphicsManager(const CGraphicsManager& other)
{

}

/** Default Assignment Operator
 */

CGraphicsManager& CGraphicsManager::operator=(const CGraphicsManager& rhs)
{
    if (this == &rhs)
    {
        return *this;
    }

    return *this;
}

/** Initializes the graphics manager. Loads configuration values and
 *  opens the window (for the first time) as well as set OpenGL stuff.
 *  \param This parameter is passed to OpenWindow as the window title.
 *  \return This return value is the return value from OpenWindow.
 */

bool CGraphicsManager::Init(const std::string& windowTitle)
{
    // Load configuration values.

    LoadConfigurationValues();

    // Open window and return result.

    return OpenWindow(windowTitle);
}

/** Loads all graphical settings that exist in the game configuration file.
 *  If the game configuration file does not exists, it will load the default
 *  values, either static in the program or based off desktop video mode.
 */

void CGraphicsManager::LoadConfigurationValues()
{
    // Note: Some values are static regardless of the values present in the
    //       game configuration file.

    // Check to see if the game configuration file exists.

    if (PHYSFS_exists(DATA_RESOURCE_FILENAME))
    {
        // If it does, load the values.

        GLFWvidmode desktopVideoMode;
        glfwGetDesktopMode(&desktopVideoMode);

        m_FullScreen = (g_Config->GetInt(CONFIG_KEY_GRAPHICS_FULLSCREEN, true) ? true : false);
        m_WindowWidth = g_Config->GetInt(CONFIG_KEY_GRAPHICS_WINDOW_WIDTH, desktopVideoMode.Width);
        m_WindowHeight = g_Config->GetInt(CONFIG_KEY_GRAPHICS_WINDOW_HEIGHT, desktopVideoMode.Height);
        m_VSync = (g_Config->GetInt(CONFIG_KEY_GRAPHICS_VSYNC, VSYNC_DEFAULT_VALUE) ? true : false);

        if (m_WindowWidth <= 0 || m_WindowHeight <= 0)
        {
            // If the given width or height are negative or zero, just default to the desktop video mode.
            // Although, passing a value of 0 for either the width or height into the glfwOpenWindow
            // function is valid, but it will default to a 640 x 480 resolution, so let's just go with
            // what the user is already using.

            m_WindowWidth = desktopVideoMode.Width;
            m_WindowHeight = desktopVideoMode.Height;
            m_RedBits = desktopVideoMode.RedBits;
            m_GreenBits = desktopVideoMode.GreenBits;
            m_BlueBits = desktopVideoMode.BlueBits;
        }
        else
        {
            // The width and height cannot be anything the user wants. Search through all the possible
            // video modes on the system and choose the one with the closest resolution and the highest
            // bits per pixel count.

            GLFWvidmode videoModes[VIDMODE_LIST_COUNT];
            int count = glfwGetVideoModes(&videoModes[0], VIDMODE_LIST_COUNT) - 1;
            int index = 0;
            int diff = 0xFFFF;

            for (; count >= 0; --count)
            {
                int tmpDiff = (m_WindowWidth - videoModes[count].Width) + (m_WindowHeight - videoModes[count].Height);

                if (tmpDiff == 0)
                {
                    index = count;
                    break;
                }

                if (diff == 0xFFFF)
                {
                    diff = tmpDiff;
                    index = count;
                    continue;
                }

                if (std::abs((double) tmpDiff) < std::abs((double) diff))
                {
                    diff = tmpDiff;
                    index = count;
                }
            }

            m_WindowWidth = videoModes[index].Width;
            m_WindowHeight = videoModes[index].Height;
            m_RedBits = videoModes[index].RedBits;
            m_GreenBits = videoModes[index].GreenBits;
            m_BlueBits = videoModes[index].BlueBits;
        }

        m_AlphaBits = VIDMODE_ALPHA_BIT_NUM;
        m_DepthBits = VIDMODE_DEPTH_BIT_NUM;
        m_StencilBits = VIDMODE_STENCIL_BIT_NUM;
        //glfwOpenWindowHint(GLFW_REFRESH_RATE, 0);
        //glfwOpenWindowHint(GLFW_ACCUM_RED_BITS, 0);
        //glfwOpenWindowHint(GLFW_ACCUM_BLUE_BITS, 0);
        //glfwOpenWindowHint(GLFW_ACCUM_GREEN_BITS, 0);
        //glfwOpenWindowHint(GLFW_ACCUM_ALPHA_BITS, 0);
        //glfwOpenWindowHint(GLFW_AUX_BUFFERS, 0);
        //glfwOpenWindowHint(GLFW_STEREO, GL_FALSE);
        glfwOpenWindowHint(GLFW_WINDOW_NO_RESIZE, GL_TRUE);
        glfwOpenWindowHint(GLFW_FSAA_SAMPLES, 4);
        glfwOpenWindowHint(GLFW_OPENGL_VERSION_MAJOR, 3);
        glfwOpenWindowHint(GLFW_OPENGL_VERSION_MINOR, 2);
        glfwOpenWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#ifdef _DEBUG
        glfwOpenWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);
#endif // _DEBUG
        glfwOpenWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    }
    else
    {
        // If it doesn't, go with the defaults.

        GLFWvidmode desktopVideoMode;
        glfwGetDesktopMode(&desktopVideoMode);

        m_FullScreen = true;
        m_WindowWidth = desktopVideoMode.Width;
        m_WindowHeight = desktopVideoMode.Height;
        m_RedBits = desktopVideoMode.RedBits;
        m_GreenBits = desktopVideoMode.GreenBits;
        m_BlueBits = desktopVideoMode.BlueBits;
        m_AlphaBits = VIDMODE_ALPHA_BIT_NUM;
        m_DepthBits = VIDMODE_DEPTH_BIT_NUM;
        m_StencilBits = VIDMODE_STENCIL_BIT_NUM;
        m_VSync = VSYNC_DEFAULT_VALUE;

        //glfwOpenWindowHint(GLFW_REFRESH_RATE, 0);
        //glfwOpenWindowHint(GLFW_ACCUM_RED_BITS, 0);
        //glfwOpenWindowHint(GLFW_ACCUM_BLUE_BITS, 0);
        //glfwOpenWindowHint(GLFW_ACCUM_GREEN_BITS, 0);
        //glfwOpenWindowHint(GLFW_ACCUM_ALPHA_BITS, 0);
        //glfwOpenWindowHint(GLFW_AUX_BUFFERS, 0);
        //glfwOpenWindowHint(GLFW_STEREO, GL_FALSE);
        glfwOpenWindowHint(GLFW_WINDOW_NO_RESIZE, GL_TRUE);
        glfwOpenWindowHint(GLFW_FSAA_SAMPLES, 4);
        glfwOpenWindowHint(GLFW_OPENGL_VERSION_MAJOR, 3);
        glfwOpenWindowHint(GLFW_OPENGL_VERSION_MINOR, 2);
        glfwOpenWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#ifdef _DEBUG
        glfwOpenWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);
#endif // _DEBUG
        glfwOpenWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    }
}

/** Saves all graphical configuration values to the game
 *  configuration file. In order for this function to execute
 *  properly, you must have called g_Config->StartWrite() before
 *  calling this function to initialize the output stream for
 *  the configuration file. You should also call g_Config->EndWrite()
 *  after you are done writing from the calling function.
 */

void CGraphicsManager::SaveConfigurationValues()
{
    g_Config->WriteValue(CONFIG_KEY_GRAPHICS_FULLSCREEN, m_FullScreen);
    g_Config->WriteValue(CONFIG_KEY_GRAPHICS_WINDOW_WIDTH, m_WindowWidth);
    g_Config->WriteValue(CONFIG_KEY_GRAPHICS_WINDOW_HEIGHT, m_WindowHeight);
    g_Config->WriteValue(CONFIG_KEY_GRAPHICS_VSYNC, m_VSync);
}

/** Opens a GLFW window. Before calling this function, you must first
 *  call LoadConfigurationValues() to set all values correctly and
 *  to set all GLFW window hints.
 *  \param Name of the title of the window. This is used during
 *  iconification as well.
 *  \return If the window is properly opened, it returns true, else it
 *  returns false.
 */

bool CGraphicsManager::OpenWindow(const std::string& windowTitle)
{
    m_WindowTitle = windowTitle;

    if (glfwOpenWindow(m_WindowWidth, m_WindowHeight, m_RedBits, m_GreenBits, m_BlueBits, m_AlphaBits, m_DepthBits, m_StencilBits, (m_FullScreen ? GLFW_FULLSCREEN : GLFW_WINDOW)) == GL_FALSE)
    {
        return false;
    }

    // Set callback functions.

    glfwSetWindowSizeCallback(&ResizeWindowCallback);

    // Set title and vsync.

    glfwSetWindowTitle(m_WindowTitle.c_str());
    glfwSwapInterval(m_VSync);

    return true;
}

/**
 */

GLuint CGraphicsManager::LoadShader(const std::string& fileName, GLenum type)
{
    GLenum err;
    GLuint id = glCreateShader(type);

    if (!id)
    {
        // Unable to create shader

        err = glGetError();

        if (err == GL_INVALID_ENUM)
        {
            m_ErrorString = "Failed to create shader: invalid enum";
        }
        else if (err == GL_INVALID_OPERATION)
        {
            m_ErrorString = "Failed to create shader: invalid operation";
        }

        return 0;
    }

    std::string source;
    std::fstream sourceStream(fileName.c_str(), std::fstream::in);

    if (sourceStream.fail())
    {
        glDeleteShader(id);
        m_ErrorString = "Failed to read shader file";
        return 0;
    }

    std::string line;

    while (std::getline(sourceStream, line))
    {
        source += "\n" + line;
    }

    sourceStream.close();

    if ((err = glGetError()) != GL_NO_ERROR)
    {
        if (err == GL_INVALID_VALUE)
        {
            m_ErrorString = "Failed to set shader source code: inva222lid value";
        }
        else if (err == GL_INVALID_OPERATION)
        {
            m_ErrorString = "Failed to set shader source code: inv222alid operation";
        }

        return 0;
    }

    char const *sourcePointer = source.c_str();
    glShaderSource(id, 1, &sourcePointer, NULL);

    if ((err = glGetError()))
    {
        if (err == GL_INVALID_VALUE)
        {
            m_ErrorString = "Failed to set shader source code: invalid value";
        }
        else if (err == GL_INVALID_OPERATION)
        {
            m_ErrorString = "Failed to set shader source code: invalid operation";
        }

        return 0;
    }

    glCompileShader(id);

    if ((err = glGetError()))
    {
        if (err == GL_INVALID_VALUE)
        {
            m_ErrorString = "Failed to set shader source code: invalid shader value";
        }
        else if (err == GL_INVALID_OPERATION)
        {
            m_ErrorString = "Failed to set shader source code: invalid operation";
        }

        return 0;
    }

    return id;
}

// Callback Functions
// These are all non-method functions due to GLFW's C style programming.

/** Handles resizing of window. This function is called when glfwOpenWindow
 *  is called and when the window size changes through glfwSetWindowSize.
 *  Those are pretty much the only cases this function will be called as
 *  the window is unable to be resized.
 *  \param Width of the window size.
 *  \param Height of the window size.
 */

void GLFWCALL ResizeWindowCallback(int width, int height)
{

}
